package de.hska.kundenverwaltung.db;

import static de.hska.kundenverwaltung.db.Kunde.FIND_ALL_NACHNAMEN;
import static de.hska.kundenverwaltung.db.Kunde.FIND_KUNDEN;
import static de.hska.kundenverwaltung.db.Kunde.FIND_KUNDEN_BY_NACHNAME;
import static de.hska.kundenverwaltung.db.Kunde.FIND_KUNDEN_BY_NACHNAME_FETCH_BESTELLUNGEN;
import static de.hska.kundenverwaltung.db.Kunde.FIND_KUNDEN_BY_NACHNAME_PREFIX;
import static de.hska.kundenverwaltung.db.Kunde.FIND_KUNDEN_ORDER_BY_ID;
import static de.hska.kundenverwaltung.db.Kunde.FIND_KUNDE_BY_ID_FETCH_BESTELLUNGEN;
import static de.hska.kundenverwaltung.db.Kunde.FIND_KUNDE_BY_USERNAME;
import static de.hska.kundenverwaltung.db.Kunde.PARAM_KUNDE_ID;
import static de.hska.kundenverwaltung.db.Kunde.PARAM_KUNDE_NACHNAME;
import static de.hska.kundenverwaltung.db.Kunde.PARAM_KUNDE_NACHNAME_PREFIX;
import static de.hska.kundenverwaltung.db.Kunde.PARAM_KUNDE_USERNAME;
import static de.hska.util.EjbConstants.PERSISTENCE_CONTEXT_HSKA;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.annotation.Resource;
import javax.ejb.Local;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import de.hska.bestellverwaltung.db.Bestellung;



/**
 *  DB-Zugriffsschicht f&uuml;r das Package "de.hska.kundenverwaltung"
 */
@Stateless
@Local(KundenverwaltungDao.class)
final public class KundenverwaltungDaoBean implements KundenverwaltungDao {
	public static final Log LOG = LogFactory.getLog(KundenverwaltungDaoBean.class);
	private static final boolean TRACE = LOG.isTraceEnabled();
	// Obergrenze fuer Windows beim Schreiben in eine Datei: 64MB - 32KB
	private static final int MAX_BYTES_WRITE = (64 * 1024 * 1024) - (32 * 1024);
	
	@PersistenceContext(name=PERSISTENCE_CONTEXT_HSKA)
	private EntityManager em;

	@Resource(name="earName")
	private String earName = "hska";    // ggf. ueberschreiben in ejb-jar.xml
	
	private final String blobDir = System.getenv("JBOSS_HOME") + File.separator + "server"
	                               + File.separator + "default" + File.separator + "deploy" + File.separator
	                               + earName + ".ear" + File.separator + earName + "Web.war" + File.separator
	                               + "images" + File.separator + "db";
	
	/* 
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<Kunde> findAllKunden() {
		Query query = em.createNamedQuery(FIND_KUNDEN);
		List<Kunde> kunden = query.getResultList();
		if (kunden == null)
			kunden = new ArrayList<Kunde>();

		return kunden;
	}
	
	/*
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<String> findAllNachnamen() {
		final Query query = em.createNamedQuery(FIND_ALL_NACHNAMEN);
		List<String> nachnamen = query.getResultList();
		return nachnamen;
	}

	/*
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<Kunde> findAllKundenId() {
		Query query = em.createNamedQuery(FIND_KUNDEN_ORDER_BY_ID);
		List<Kunde> kunden = query.getResultList();
		if (kunden == null)
			kunden = new ArrayList<Kunde>();

		return kunden;
	}

	/*
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<Kunde> findKundenByNachname(String nachname) throws KundeNotFoundException {
		final Query query = em.createNamedQuery(FIND_KUNDEN_BY_NACHNAME);
		query.setParameter(PARAM_KUNDE_NACHNAME, nachname);
		final List<Kunde> kunden = query.getResultList();   // Query operiert auf Cache

		if (kunden == null || kunden.isEmpty()) {
			final KundeNotFoundException e = new KundeNotFoundException(nachname);
			LOG.warn(e.getMessage());
			throw e;
		}

		return kunden;
	}


	/*
	 * {@inheritDoc}
	 */
	//TODO Doppelter Fetch-Join nicht moeglich mit Hibernate 3.2
	@SuppressWarnings("unchecked")
	public List<Kunde> findKundenMitBestellungenUndLieferungenByNachname(String nachname, boolean mitLieferungen)
		               throws KundeNotFoundException {
		// Bestellungen mitladen
		Query query = em.createNamedQuery(FIND_KUNDEN_BY_NACHNAME_FETCH_BESTELLUNGEN);
		query.setParameter(PARAM_KUNDE_NACHNAME, nachname);
		final List<Kunde> kunden = query.getResultList();

		if (kunden == null || kunden.isEmpty()) {
			final KundeNotFoundException e = new KundeNotFoundException(nachname);
			LOG.warn(e.getMessage());
			throw e;
		}
		
		if (mitLieferungen) {
			// 2. Fetch-Join als dynamische Query
			final StringBuilder sb = new StringBuilder("SELECT DISTINCT b FROM Bestellung b LEFT JOIN FETCH b.lieferungen WHERE ");
			boolean mindEineBestellung = false;
			for (Kunde k: kunden) {
				final Collection<Bestellung> bestellungen = k.getBestellungen();
				for (Bestellung b: bestellungen) {
					mindEineBestellung = true;
					sb.append("b.id = ");
					sb.append(b.getId());
					sb.append(" OR ");
				}
			}
				
			if (mindEineBestellung) {
				// Letztes OR eliminieren
				final int length = sb.length();
				sb.delete(length-4, length-1);
				final String queryStr = sb.toString();
				if (TRACE) LOG.trace(queryStr);
				query = em.createQuery(queryStr);
				query.getResultList();
			}
		}
		
		return kunden;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<Kunde> findKundenByNachnamePrefix(String nachnamePrefix) {
		final Query query = em.createNamedQuery(FIND_KUNDEN_BY_NACHNAME_PREFIX);
		query.setParameter(PARAM_KUNDE_NACHNAME_PREFIX, nachnamePrefix + '%');
		List<Kunde> kunden = query.getResultList();   // Query operiert auf Cache

		if (kunden == null) {
			kunden = new ArrayList<Kunde>(0);
		}

		return kunden;
	}


	/*
	 * {@inheritDoc}
	 */
	public Kunde findKundeById(Long id) throws KundeNotFoundException {
		final Kunde kunde = em.find(Kunde.class, id);
		if (kunde == null) {
			final KundeNotFoundException e = new KundeNotFoundException(id);
			throw e;
		}

		return kunde;
	}

	/*
	 * {@inheritDoc}
	 */
	public Kunde findKundeMitBestellungenById(Long id) throws KundeNotFoundException {
		final Query query = em.createNamedQuery(FIND_KUNDE_BY_ID_FETCH_BESTELLUNGEN);
		query.setParameter(PARAM_KUNDE_ID, id);

		Kunde kunde = null;
		try {
			kunde = (Kunde) query.getSingleResult();
		}
		catch (NoResultException e) {
			final KundeNotFoundException e2 = new KundeNotFoundException(id);
			throw e2;
		}

		return kunde;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Kunde findKundeByUserName(String username) throws KundeNotFoundException {
		final Query query = em.createNamedQuery(FIND_KUNDE_BY_USERNAME);
		query.setParameter(PARAM_KUNDE_USERNAME, username);

		Kunde kunde = null;
		try {
			kunde = (Kunde) query.getSingleResult();
		}
		catch (NoResultException e) {
			final KundeNotFoundException e2 = new KundeNotFoundException(username);
			LOG.warn(e2.getMessage());
			throw e2;
		}

		return kunde;
	}


	/*
	 * {@inheritDoc}
	 */
	public Collection<Kunde> updateKunden(Collection<Kunde> kunden) throws KundeDeletedException, KundeUpdatedException {
		if (kunden == null || kunden.isEmpty()) {
			return new ArrayList<Kunde>(0);
		}

		Collection<Kunde> aktualisierteKunden = new ArrayList<Kunde>(kunden.size());
		for (Kunde k : kunden) {
			if (TRACE) LOG.trace(k);
			
			// Wurde k konkurrierend geloescht ?
			final Kunde tmp = em.find(Kunde.class, k.getId());
			if (TRACE) LOG.trace(tmp);
			if (tmp == null) {
				final KundeDeletedException e = new KundeDeletedException(k.getId());
				LOG.error(e.getMessage());
				throw e;
			}
			
			try {
				k = em.merge(k);
			}
			catch (OptimisticLockException e) {
				final KundeUpdatedException e2 = new KundeUpdatedException(k.getId(), e); 
				throw e2;
			}
			aktualisierteKunden.add(k);
		}

		return aktualisierteKunden;
	}


	/*
	 * {@inheritDoc}
	 */
	public void deleteKunden(Collection<Kunde> kunden) {
		if (kunden == null || kunden.isEmpty()) {
			return;
		}

		for (Kunde k: kunden) {			
			if (k == null)
				continue;

			if (TRACE) LOG.trace(k);
			
			// Wenn ein zu loeschendes Objekt detached ist,
			// kann es nicht geloescht werden
			if (!em.contains(k)) {
				k = em.find(Kunde.class, k.getId());
				if (k == null)
					continue;
			}
			
			em.remove(k);
		}
	}


	/*
	 * {@inheritDoc}
	 */
	public Collection<Kunde> insertKunden(Collection<Kunde> kunden) {
		if (kunden == null || kunden.isEmpty()) {
			return new ArrayList<Kunde>(0);
		}
		if (TRACE) LOG.trace(kunden.size());

		for (Kunde k: kunden) {
			if (TRACE) LOG.trace(k);
			em.persist(k);
		}

		return kunden;
	}

	/*
	 * {@inheritDoc}
	 */
	public Kunde updateKundeMitBild(Kunde k, byte[] bildBytes, String extension) throws BildException {
		// Dateiname mit Extension ermitteln
		final String neuerDateiname = k.getClass().getSimpleName() + "_" + k.getId() + "_" + k.getVersion() + '.' + extension;
		
		// Bild zum gefundenen Kunden abspeichern
		
		// NIO (= New I/O) verwenden: Datenbloecke direkt auf die Platte schreiben
		// http://articles.techrepublic.com.com/5100-10878_11-6095050.html
		// http://java.sun.com/javase/6/docs/technotes/guides/io/index.html
		final ByteBuffer byteBuffer = ByteBuffer.wrap(bildBytes);
		FileChannel outChannel = null;
		try {
			final File neueDatei = new File(blobDir + File.separator + neuerDateiname);
			outChannel = new FileOutputStream(neueDatei).getChannel();
			outChannel.write(byteBuffer);
		}
		catch (IOException e) {
			final BildException e2 = new BildException(k.getId(), neuerDateiname);
			LOG.error(e2.getMessage());
			throw e2;
		}
		finally {
			try {
				if (outChannel != null) {
					outChannel.close();
				}
			}
			catch (IOException e) {
				LOG.error(e);
			}
		}

		final String alterDateiname = k.getDateiname();
		k.setDateiname(neuerDateiname);
		
		// Alte Datei ggf. loeschen
		if (alterDateiname != null && !alterDateiname.equals("")) {
			final File alteDatei = new File(blobDir + File.separator + alterDateiname);
			alteDatei.delete();
		}

		return k;
	}
	
	/*
	 * {@inheritDoc}
	 */
	public Kunde updateKundeMitBild(Kunde k, File bildTmpFile, String extension) throws BildException {
		if (bildTmpFile == null)
			return null;
		
		// Dateiname mit Extension ermitteln
		final String neuerDateiname = k.getClass().getSimpleName() + "_" + k.getId() + "_" + k.getVersion() + '.' + extension;
		
		// Bild zum gefundenen Kunden abspeichern
		
		// NIO (= New I/O) verwenden: Datenbloecke direkt auf die Platte schreiben, Byte-Arrays nicht notwendig
		// http://articles.techrepublic.com.com/5100-10878_11-6095050.html
		// http://java.sun.com/javase/6/docs/technotes/guides/io/index.html
		FileChannel inChannel = null;
		FileChannel outChannel = null;
		try {
			inChannel = new FileInputStream(bildTmpFile).getChannel();
			final File neueDatei = new File(blobDir + File.separator + neuerDateiname);
			outChannel = new FileOutputStream(neueDatei).getChannel();
			final long dateiGroesse = inChannel.size();
			for (long position = 0; position < dateiGroesse; ) {
				position += inChannel.transferTo(position, MAX_BYTES_WRITE, outChannel);
			}
		}
		catch (IOException e) {
			final BildException e2 = new BildException(k.getId(), neuerDateiname);
			LOG.error(e2.getMessage());
			throw e2;
		}
		finally {
			try {
				if (inChannel != null) {
					inChannel.close();
				}
				if (outChannel != null) {
					outChannel.close();
				}
				bildTmpFile.delete();
			}
			catch (IOException e) {
				LOG.error(e);
			}
		}

		final String alterDateiname = k.getDateiname();
		k.setDateiname(neuerDateiname);
		
		// Alte Datei ggf. loeschen
		if (alterDateiname != null && !alterDateiname.equals("")) {
			final File alteDatei = new File(blobDir + File.separator + alterDateiname);
			alteDatei.delete();
		}

		return k;
	}
}
